feat(share): short spoken codes for live session invites - #135
Merged
Conversation
Six call sites resolved the server URL and JWT with the same four lines and the same two error keys. listActiveSessions keeps its own copy: it degrades to an empty list instead of throwing.
The server returns invite_token once, at creation, and it was held only in ShareMenu's local state — so reopening the menu on a session that was already sharing showed an empty Link tab, with no way back to the link short of stopping and restarting the share. Retain it on the connection instead.
Hosts can mint a 10-character Crockford code for an invite-link session and read it down a phone line; the server kills it after ten minutes, so what lands in chat scrollback or a clipboard manager is a dead credential rather than a session-lifetime token. Minted on demand, not alongside the link, so the window starts when the host needs it. Guests can paste a code anywhere the other invite shapes already work: detection stays synchronous on shape, and only activating a join costs a request. Joining by a bare sessionId:token now requires a real session id. TeamSessions previously accepted any colon-separated pair, unlike OmniSearch, which had always been strict; one existing test carried the looser contract and moved to a real UUID.
Closed
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Client half of #65: hosts can mint a short code for a live session, and guests can join with one. The server half is already merged (VoltiusApp/server#7) and live in production.
What a host sees
The Link tab keeps the
voltius://joindeep link exactly as it is — same bytes displayed and copied, same styling, still auto-copied on generate. Underneath it, a second row appears:K7M2-P9QX-3B, a copy button,expires in 9:42, and New codePlacement was chosen from mockups rather than guessed. Two alternatives were rejected: a Link/Code toggle sharing one field (it makes the displayed and copied values mode-dependent — the exact divergence PR #114 had to fix), and replacing the link outright (it would delete the clickable artifact #69 just shipped).
The code is minted on demand, not alongside the link, so its ten minutes start when the host actually needs to say it.
InviteCodeFieldis reused as-is for the code row, so display-equals-clipboard holds by construction rather than by a second implementation.What a guest sees
A code works anywhere the existing invite shapes already work — the Join by code modal and the omni palette. Detection stays synchronous on shape (10 Crockford symbols,
I/L→1,O→0), so typing in search never hits the API; only activating a join costs a request.src/services/resolveJoinInput.tsis the single place that turns any of the three shapes into{ sessionId, inviteToken }. Both call sites use it, so the branch exists once.parseInviteCodewas deliberately left pure and synchronous — a short code cannot be resolved locally, so making it the "third form" inside that function would have forced every caller to become async.Error mapping is honest about what the server can distinguish: unknown, malformed, expired and revoked codes all answer 404, so they all read "expired or incorrect". 429 gets its own message.
Two changes beyond the code row
inviteTokenis now retained on the connection. The server returns it once, at creation, and it was held only in ShareMenu's local state — so reopening the menu on a session that was already sharing showed an empty Link tab, with no route back to the link short of stopping and restarting the share. The first Generate still requires an explicit click, because it creates the multiplayer session (publishes the terminal, consumes a concurrent-session slot); only an already sharing session now shows its link automatically.Joining by a bare
sessionId:tokenrequires a real session id.TeamSessionspreviously accepted any colon-separated pair while OmniSearch was always strict, sohost:22could reach the join call there. The two now agree. One existing test carried the looser contract and moved to a real UUID — called out because it is a test changed to match tightened behaviour, not a test fixed to match a bug.Verification
tsc --noEmitclean;npm run build(tsc + plugin bundles + vite) clean.Uis rejected), 6 service tests, 6 resolver tests, 8SpokenCodeRowtests covering mint-on-demand, grouping, countdown, expiry-drop, regenerate, failure, and interval cleanup on unmount.The countdown test was genuinely flaky before landing: vitest's
shouldAdvanceTimelet real wall time into the expiry arithmetic, so it passed until real UTC crossed the fixture's expiry and then failed at random. It now fakesDatealongside the timers and advances only when told, and survived six consecutive runs.Not verified — live click-through
No live UI pass was done. Reaching the new row requires a logged-in Pro account and a real shared session, and the running headless container mounts the primary checkout rather than this worktree. So the mint button, the countdown against a real server clock, and an actual code-based join between two accounts are unproven in the app; unit tests structurally cannot cover them. Worth a pass with the two-account harness before this is considered done.
Known gap
A failed redeem in the omni palette only reaches
console.error, because the palette closes on activation. The Join by code modal surfaces the error properly. Left as-is rather than restructuring the palette's dismissal in this PR.